# 53. 协程、Greenlet模块、Gevent模块

# 协程

之前我们学习了线程、进程的概念,了解了在操作系统中进程是资源分配的最小单位,线程是CPU调度的最小单位。

按道理来说我们已经算是把cpu的利用率提高很多了。

但是我们知道无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。

随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发。

这样就可以节省创建线进程所消耗的时间。

为此我们需要先回顾下并发的本质:切换+保存状态

cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

  1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
  2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

# 通过yield来验证程序的运行保存状态

为此我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法,我们来简单复习一下:

import time
def conn():
    while 1:
        n = yield  
        print('干掉了%s'%n)

def pro():
    n = conn()
    next(n)
    for i in range(10000):
        print('生产了%s' % i)
        n.send(i)

start = time.time()
pro()
print(time.time() - start)


执行结果:
生产了9999
干掉了9999
0.176527738571167

# yield可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
# send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换

# 在单线程中,如果存在多个函数,如果有某个函数发生IO操作,我想让程序马上切换到另一个函数去执行
# 以此来实现一个假的并发现象。

# yield实验总结

总结:

  1. yield 只能实现单纯的切换函数和保存函数状态的功能
  2. 不能实现:当某一个函数遇到io阻塞时,自动的切换到另一个函数去执行
  3. 目标是:当某一个函数中遇到IO阻塞时,程序能自动的切换到另一个函数去执行
  4. 如果能实现这个功能,那么每个函数都是一个协程
  5. 但是 协程的本质还是主要依靠于yield去实现的。
  6. 如果只是拿yield去单纯的实现一个切换的现象,你会发现,跟本没有程序串行执行效率高
  7. 就算使用了yield来简单实现了IO切换跟保存任务状态,但是这种切换也需要时间

注意:yield是无法做到IO阻塞时自动切换的

# 串行执行比上一个实验速度更快

import time
def pro():
    l = []
    for i in range(10000):
        l.append(i)
    return l

def consumer(l):
    for i in l:
        print(i)
start = time.time()
l = pro()
consumer(l)
print(time.time() - start)

执行结果:
9998
9999
0.04886507987976074

# 为什么要有协程?

因为想要在单线程内实现并发的效果:

  1. 因为CPthon有GIL锁,限制了在同一个时间点,只能执行一个线程

  2. 所以想要在执行一个线程的期间,充分的利用CPU的性能

  3. 所以才有了想在单线程内实现并发的效果。

    1. 并发:切换+保存状态
      1. cpu是为什么要切换?
        1. 因为某个程序阻塞了
        2. 因为某个程序用完了时间片
  4. 很明显 解决GIL锁这个问题才能提高效率

  5. 所以想要实现单线程的并发,就要解决在单线程内,多个任务函数中,某个任务函数遇见IO操作,马上自动切换到其他任务函数去执行。

# 协程总结

对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)

控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,

让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

  1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
  2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换

对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:

  1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
  2. 单线程内就可以实现并发的效果,最大限度地利用cpu

总结协程特点:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
  5. 协程:是一个比线程更加轻量级的单位,是组成线程的各个函数,协程本身没有实体

# Greenlet模块 - 函数跟函数之间切换

能简单的实现函数与函数之间的切换,但是遇到IO操作,不能自动切换到其他函数中

需要安装 :greenlet模块

# Greenlet的简单功能介绍

from greenlet import greenlet
f1 = greenlet(func) ## 注册一下函数func,将函数注册成一个对象f1

f1.switch() ## 调用func,如果func需要传参,就在switch这里传即可

# Greenlet的简单实验

from greenlet import greenlet
import time

def eat(name):
    print('%s吃炸鸡'%name)
    time.sleep(2)
    f2.switch('小雪2')  ## 当执行到这行,就切换到drink函数去,并传一个参数
    print('%s吃雪糕'%name)
    f2.switch()   ## 当执行到这行,就切换到drink函数去

def drink(name):
    print('%s喝啤酒'%name)
    f1.switch()   ## 当执行到这行,就切换到eat函数去
    print('%s喝可乐'%name)


f1 = greenlet(eat)  ## 实验化对象
f2 = greenlet(drink) ## 实验化对象
f1.switch('小雪')

执行结果:
小雪吃炸鸡
小雪2喝啤酒
小雪吃雪糕
小雪2喝可乐

# greenlet 只是可以实现一个简单的切换功能,还是不能做到遇到IO就切换
# g1 = greenlet(func)   实例化一个对象
# g1.switch()  用这种方式去调用func函数
# 当使用switch调用func的时候,什么时候func会停止运行?
#   1 要么return      2 要么在func内部又遇到 switch

但是如果遇到IO阻塞时无法自动切换到另一个程度去,让另一个程序去执行

# Gevent模块

可以实现在某函数内部遇到IO操作,就自动的切换到其他函数内部去执行

需要安装:gevent模块

注意:gevent不能识别其他的IO操作,只能识别自己认识的IO,自己的IO:gevent.sleep(1)

# Gevent的简单功能介绍

g = gevent.spawn(func,参数) ## 注册一下函数func,返回一个对象g
gevent.join(g)  ## 等待g指向的函数func执行完毕,如果在执行过程中,遇到IO,就切换
gevent.joinall([g1,g2,g3]) ## 等待g1 g2 g3指向的函数func执行完毕,需要一个列表类型,里面放置需要等待的对象
gevent.sleep(1) ## 暂停1秒,gevent内部的状态阻塞器

# 实验 - 遇到IO操作时1

遇到gevent自带的IO操作时会自动切换

import gevent
def func1():
    print('1 2 3 4')
    gevent.sleep(1)
    print('3 2 3 4')

def func2():
    print('2 2 3 4')
    gevent.sleep(1)
    print('再来一次')

g1 = gevent.spawn(func1)
g2 = gevent.spawn(func2)
g1.join()# 等待g1指向的任务执行结束
g2.join()# 等待g2指向的任务执行结

执行结果:
1 2 3 4
2 2 3 4
3 2 3 4
再来一次

# 实验 - 遇到IO操作时2

遇到不是gevent自带的IO操作时不会自动切换

import gevent
import time
def func1():
    print('1 2 3 4')
    time.sleep(1) # gevent不能识别其他的IO操作,只能识别自己认识的IO
    print('3 2 3 4')

def func2():
    print('2 2 3 4')
    time.sleep(1)  # gevent不能识别其他的IO操作,只能识别自己认识的IO
    print('再来一次')

g1 = gevent.spawn(func1)
g2 = gevent.spawn(func2)
g1.join()# 等待g1指向的任务执行结
g2.join()# 等待g2指向的任务执行结

执行结果:
1 2 3 4
3 2 3 4
2 2 3 4
再来一次

## 通过执行结果就能看出,执行的顺序是,先执行func1函数后在执行func2函数

# 实验 - 使用patch_all()方法能识别另种IO操作

使用patch_all()方法能识别大部分的IO操作

from gevent import monkey
monkey.patch_all() # 可以让gevent识别大部分常用的IO操作
import time

def func1():
    print('1 2 3 4')
    time.sleep(1)
    print('3 2 3 4')

def func2():
    print('2 2 3 4')
    time.sleep(1)
    print('再来一次')

g1 = gevent.spawn(func1)
g2 = gevent.spawn(func2)
g1.join() # 等待g1指向的任务执行结束
g2.join() # 等待g2指向的任务执行结

执行结果:
1 2 3 4
2 2 3 4
3 2 3 4
再来一次

# 实验 - 爬虫示例 - 串行跟协程的区别

串行 == 同步

协程 == 异步

from gevent import monkey
monkey.patch_all()
import gevent
import time
import requests

def get_result(url):# 任务函数
    res = requests.get(url)
    print(url,res.status_code,len(res.text))

url_l = ['http://www.baidu.com',
         'https://www.jd.com',
         'http://www.apache.com',
         'http://www.taobao.com',
         'http://www.qq.com',
         'http://www.mi.com',
         'http://www.cnblogs.com']

def sync_func(url_l):
    '''同步调用'''
    for url in url_l:# 串行执行任务函数
        get_result(url)

def async_func(url_l):
    '''异步'''
    l = []
    for url in url_l:
        g = gevent.spawn(get_result,url)# 使用gevent,协程去并发实现执行任务函数
        # 当遇见请求某个网页发生比较大的网络延迟(IO),马上会切换到其他的任务函数
        l.append(g)
    gevent.joinall(l)# 等待所有任务函数执行结束

start = time.time()
sync_func(url_l)
print('串行:',time.time() - start)

start = time.time()
async_func(url_l)
print('协程:',time.time() - start)

执行结果:
http://www.baidu.com 200 2381
https://www.jd.com 200 103519
http://www.apache.com 403 27610
http://www.taobao.com 200 138584
http://www.qq.com 200 221984
http://www.mi.com 200 277187
http://www.cnblogs.com 200 42823
串行: 2.7305195331573486
    
http://www.baidu.com 200 2381
https://www.jd.com 200 103519
http://www.qq.com 200 221984
http://www.taobao.com 200 138616
http://www.mi.com 200 277187
http://www.cnblogs.com 200 42823
http://www.apache.com 403 27610
协程: 1.8869080543518066